Skip to content

fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions - #1602

Merged
thymikee merged 2 commits into
mainfrom
fix/1597-ambiguous-match-candidates
Aug 4, 2026
Merged

fix: surface AMBIGUOUS_MATCH candidates and name find's supported actions#1602
thymikee merged 2 commits into
mainfrom
fix/1597-ambiguous-match-candidates

Conversation

@thymikee

@thymikee thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #1597 — two error-UX gaps where an agent got a bare error with nothing to act on.

1. AMBIGUOUS_MATCH now lists candidates. buildAmbiguousMatchError (the single producer for element-match ambiguity — src/daemon/handlers/find.ts) now builds details.candidates from formatSnapshotLine, the same compact renderer snapshot -i uses: @ref [role] "label/identifier". Capped at 5 (AMBIGUOUS_MATCH_CANDIDATE_LIMIT); details.matches keeps the true total.

Critically, the candidates previously lived only in details, which neither text surface an agent actually reads ever printed — the CLI's printHumanError and MCP's formatToolErrorText both stopped at Error: ... + Hint: .... A new shared renderer, formatAmbiguousMatchCandidateLines (src/utils/output.ts), prints the candidate lines unconditionally (not gated behind --debug) on both surfaces, with a +N more marker when the cap trimmed the list:

Error (AMBIGUOUS_MATCH): find matched 6 elements for text "Follow". Use a more specific locator or selector.
Hint: Multiple candidates matched. Narrow the query or pass an exact identifier.
Candidates:
  @e2 [button] "Follow"
  @e3 [button] "Follow"
  @e4 [button] "Follow"
  @e5 [button] "Follow"
  @e6 [button] "Follow"
  +1 more

I audited every AMBIGUOUS_MATCH emission site (grep -rn "AMBIGUOUS_MATCH") to confirm buildAmbiguousMatchError really is the one shared producer for element-match ambiguity — the other two throw sites (dispatch-resolve.ts device selection, session-doctor-app.ts app selection) are a different domain (devices/apps, not elements) and already carry their own appropriate candidate shape. The direct-iOS-runner fast path for wait/get/is can also raise a bare runner-native AMBIGUOUS_MATCH (ADR 0011, deliberately not delegated) — that one has no node list available at the point it's raised, so enriching it is out of scope here and its existing tests are untouched.

2. find <text> <action> with an unsupported action now hints at the fix. find <text> press used to return a bare INVALID_ARGS: Unsupported find action: press. There were actually two throw sites for this (packages/selectors/src/internal/find.ts's raw-token parser, reachable from direct daemon/MCP dispatch, and src/commands/interaction/selectors.ts's typed CLI reader, reachable from the terminal) — both now attach the same hint via one shared exported constant, UNSUPPORTED_FIND_ACTION_HINT:

find actions: click (default), focus, fill, type, exists, wait, get text, get attrs — there is no press/longpress/swipe find action. Run find "" to list matches, then act on the resolved @ref directly, e.g. press @enn.

Matching semantics are unchanged — ambiguous rejection stays by-design; this is purely an error-message/UX fix.

Red evidence

Before this change:

  • buildAmbiguousMatchError capped candidates at 8 and formatted them as @e2(Follow) inside details.candidates — but neither printHumanError nor formatToolErrorText read that field at all, so the agent-visible text was just Error (AMBIGUOUS_MATCH): find matched N elements... \n Hint: Multiple candidates matched... with no refs.
  • Unsupported find action: press had no details.hint, so it fell through to the generic INVALID_ARGS default hint ("Check command arguments and run --help for usage examples."), with no mention of what find does support or how to recover.

New tests in src/utils/__tests__/output.test.ts, src/mcp/__tests__/tool-error.test.ts, src/daemon/handlers/__tests__/find.test.ts, src/daemon/handlers/__tests__/find-args.test.ts, and src/commands/interaction/selectors.test.ts assert the new candidate lines / hint text directly and would fail against the old shapes.

The help-conformance benchmark corpus's AMBIGUOUS_MATCH quiz case previously taught models the opposite lesson on purpose ("candidate refs live in details, which the human rendering does not print"); it's updated to reflect that candidates now print, while keeping its core lesson intact (all 3 sample candidates share an identical label, so picking a specific @eN from this output alone is still a guess).

Test plan

  • pnpm check:quick (lint + typecheck) — clean
  • pnpm check:layering — clean (added UNSUPPORTED_FIND_ACTION_HINT to the pinned @agent-device/selectors façade symbol list)
  • pnpm format:check — clean
  • Targeted vitest files (find/output/tool-error/help-conformance) — all pass
  • pnpm test:unit — 5297/5299 pass; the 2 failures (src/daemon/__tests__/runtime-hints.test.ts, unrelated Android/adb mocked tests) pass cleanly in isolation (15/15) — pre-existing timeout flakiness under parallel CPU contention, not touched by this change

🤖 Generated with Claude Code

…ions (#1597)

AMBIGUOUS_MATCH errors now list the matching candidates (ref, role,
label/identifier) rendered the same way as snapshot -i lines, capped at
5 with a "+N more" marker. buildAmbiguousMatchError (the single
producer, src/daemon/handlers/find.ts) reuses formatSnapshotLine to
build the list; formatAmbiguousMatchCandidateLines (src/utils/output.ts)
renders it unconditionally on both text surfaces an agent actually
reads (CLI printHumanError and MCP formatToolErrorText) — previously
the candidates lived only in details, which neither surface printed.

find's "Unsupported find action: X" (e.g. from `find <text> press`)
now attaches a hint naming every action find actually supports and the
two-step recovery shape: run find "<text>" to resolve the ref, then
dispatch the gesture as its own command (press @enn). The hint is a
single exported constant (UNSUPPORTED_FIND_ACTION_HINT) shared by both
throw sites — packages/selectors' raw-token parser and the CLI's typed
reader (src/commands/interaction/selectors.ts) — so they can't drift.

Matching semantics are unchanged; ambiguous rejection stays by-design.
The help-conformance corpus's AMBIGUOUS_MATCH quiz is updated: its
premise ("candidate refs were not shown") no longer holds, but with 3
identically-labeled candidates the lesson (don't guess a specific ref)
still holds.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.96 MB 1.96 MB +555 B
JS gzip 627.1 kB 627.3 kB +192 B
npm tarball 748.7 kB 749.5 kB +710 B
npm unpacked 2.62 MB 2.62 MB +3.3 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.2 ms 25.8 ms -0.5 ms
CLI --help 62.8 ms 62.6 ms -0.2 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/sdk-batch-runner.js +243 B +122 B
dist/src/viewport-dimension.js +328 B +92 B
dist/src/context.js 0 B -13 B
dist/src/registry.js +16 B +9 B
dist/src/cli.js -4 B -6 B

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

P2: The new candidate renderer is called unconditionally for every normalized error and stringifies any details.candidates array. Existing device resolution errors use object candidates { id, name } for both APP_NOT_INSTALLED and device AMBIGUOUS_MATCH, so CLI and MCP will now print Candidates: [object Object]. Restrict the element renderer to the find handler's recognized all-string shape (for example also require numeric matches), or add a typed renderer for device candidates, with a regression test for the existing device-error shape.

Exact-head Fallow is also owner-action red because AMBIGUOUS_MATCH_CANDIDATE_LIMIT is exported but has no external consumer; keep it module-local unless the export is intentional and pinned.

…in shapes

Review on #1602 (P2): formatAmbiguousMatchCandidateLines ran for every
normalized error and stringified details.candidates unconditionally,
but device-domain AMBIGUOUS_MATCH/APP_NOT_INSTALLED errors
(findBootedAppleSimulatorWithApp, src/core/dispatch-resolve.ts) reuse
that key for { id, name } device objects with no `matches` field —
CLI and MCP would have printed "Candidates: [object Object]" for
those. The renderer now requires numeric details.matches AND every
candidate to be a string before rendering anything, restricting it to
buildAmbiguousMatchError's element-match shape; unrecognized shapes
render nothing, same as before this feature existed. Added regression
tests against the exact device-error shape on both text surfaces.

Also unexports AMBIGUOUS_MATCH_CANDIDATE_LIMIT (fallow flagged it as
an unused production export) — it has no consumer outside find.ts.
@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Addressed both P2 findings in 724e603:

  1. [object Object] on device-domain candidates. formatAmbiguousMatchCandidateLines now requires typeof details.matches === 'number' and every entry in details.candidates to be a string before rendering anything — both must hold, not just one. findBootedAppleSimulatorWithApp's device-domain AMBIGUOUS_MATCH/APP_NOT_INSTALLED (src/core/dispatch-resolve.ts) reuses details.candidates for { id, name } objects and never sets details.matches, so it now fails both guards and renders nothing — the same behavior it had before this renderer existed. Added regression tests against that exact error shape on both the CLI (printHumanError) and MCP (formatToolErrorText) text paths, asserting no Candidates: block and no [object Object].

  2. Unpinned export. AMBIGUOUS_MATCH_CANDIDATE_LIMIT had no consumer outside find.ts, so it's now module-local (const, not export const). pnpm check:production-exports is clean.

Re-ran pnpm check:quick, pnpm format:check, pnpm check:layering, pnpm check:production-exports, and the targeted vitest files (src/utils/__tests__/output.test.ts, src/mcp/__tests__/tool-error.test.ts, src/daemon/handlers/__tests__/find.test.ts, find-args.test.ts, src/commands/interaction/selectors.test.ts, packages/selectors/src/internal/find.test.ts) — all green.

🤖 Addressed by Claude Code

@thymikee

thymikee commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Re-review at 724e60383: prior findings are resolved. Candidate rendering is now restricted to the intended numeric-matches + all-string element shape; CLI and MCP pin the existing device-object error shape; the cap is module-local; and exact-head CI is fully green. Code review is clean and this is ready for human review.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 4, 2026
@thymikee
thymikee merged commit 8ba5f9b into main Aug 4, 2026
31 checks passed
@thymikee
thymikee deleted the fix/1597-ambiguous-match-candidates branch August 4, 2026 19:10
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-04 19:10 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AMBIGUOUS_MATCH and unsupported find actions give agents nothing to recover with

1 participant